drawing

Проект: Анализ резюме из HeadHunter

In [ ]:
# импортируем библиотеки
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px

# настроим вывод графиков
import plotly.io as pio
pio.renderers.default='notebook'

Исследование структуры данных¶

  1. Читаем данные
In [ ]:
hh_data = pd.read_csv('dst-3.0_16_1_hh_database.csv', sep=';')
  1. Вывод первых строк
In [ ]:
hh_data.head(5)
Out[ ]:
Пол, возраст ЗП Ищет работу на должность: Город, переезд, командировки Занятость График Опыт работы Последнее/нынешнее место работы Последняя/нынешняя должность Образование и ВУЗ Обновление резюме Авто
0 Мужчина , 39 лет , родился 27 ноября 1979 29000 руб. Системный администратор Советск (Калининградская область) , не готов к... частичная занятость, проектная работа, полная ... гибкий график, полный день, сменный график, ва... Опыт работы 16 лет 10 месяцев Август 2010 — п... МАОУ "СОШ № 1 г.Немана" Системный администратор Неоконченное высшее образование 2000 Балтийск... 16.04.2019 15:59 Имеется собственный автомобиль
1 Мужчина , 60 лет , родился 20 марта 1959 40000 руб. Технический писатель Королев , не готов к переезду , готов к редким... частичная занятость, проектная работа, полная ... гибкий график, полный день, сменный график, уд... Опыт работы 19 лет 5 месяцев Январь 2000 — по... Временный трудовой коллектив Менеджер проекта, Аналитик, Технический писатель Высшее образование 1981 Военно-космическая ак... 12.04.2019 08:42 Не указано
2 Женщина , 36 лет , родилась 12 августа 1982 20000 руб. Оператор Тверь , не готова к переезду , не готова к ком... полная занятость полный день Опыт работы 10 лет 3 месяца Октябрь 2004 — Де... ПАО Сбербанк Кассир-операционист Среднее специальное образование 2002 Профессио... 16.04.2019 08:35 Не указано
3 Мужчина , 38 лет , родился 25 июня 1980 100000 руб. Веб-разработчик (HTML / CSS / JS / PHP / базы ... Саратов , не готов к переезду , готов к редким... частичная занятость, проектная работа, полная ... гибкий график, удаленная работа Опыт работы 18 лет 9 месяцев Август 2017 — Ап... OpenSoft Инженер-программист Высшее образование 2002 Саратовский государст... 08.04.2019 14:23 Не указано
4 Женщина , 26 лет , родилась 3 марта 1993 140000 руб. Региональный менеджер по продажам Москва , не готова к переезду , готова к коман... полная занятость полный день Опыт работы 5 лет 7 месяцев Региональный мене... Мармелад Менеджер по продажам Высшее образование 2015 Кгу Психологии и педаг... 22.04.2019 10:32 Не указано
  1. Основная информация о числе непустых значений в столбцах и их типах в таблице.
In [ ]:
hh_data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 44744 entries, 0 to 44743
Data columns (total 12 columns):
 #   Column                           Non-Null Count  Dtype 
---  ------                           --------------  ----- 
 0   Пол, возраст                     44744 non-null  object
 1   ЗП                               44744 non-null  object
 2   Ищет работу на должность:        44744 non-null  object
 3   Город, переезд, командировки     44744 non-null  object
 4   Занятость                        44744 non-null  object
 5   График                           44744 non-null  object
 6   Опыт работы                      44576 non-null  object
 7   Последнее/нынешнее место работы  44743 non-null  object
 8   Последняя/нынешняя должность     44742 non-null  object
 9   Образование и ВУЗ                44744 non-null  object
 10  Обновление резюме                44744 non-null  object
 11  Авто                             44744 non-null  object
dtypes: object(12)
memory usage: 4.1+ MB
  1. Сумма непустых значений
In [ ]:
hh_data.isnull().sum()
Out[ ]:
Пол, возраст                         0
ЗП                                   0
Ищет работу на должность:            0
Город, переезд, командировки         0
Занятость                            0
График                               0
Опыт работы                        168
Последнее/нынешнее место работы      1
Последняя/нынешняя должность         2
Образование и ВУЗ                    0
Обновление резюме                    0
Авто                                 0
dtype: int64
  1. Основная статистическая информация о столбцах.
In [ ]:
hh_data.describe(include='object')
Out[ ]:
Пол, возраст ЗП Ищет работу на должность: Город, переезд, командировки Занятость График Опыт работы Последнее/нынешнее место работы Последняя/нынешняя должность Образование и ВУЗ Обновление резюме Авто
count 44744 44744 44744 44744 44744 44744 44576 44743 44742 44744 44744 44744
unique 16003 690 14929 10063 38 47 44413 30214 16927 40148 18838 2
top Мужчина , 32 года , родился 17 сентября 1986 50000 руб. Системный администратор Москва , не готов к переезду , не готов к кома... полная занятость полный день Опыт работы 10 лет 8 месяцев Апрель 2018 — по... Индивидуальное предпринимательство / частная п... Системный администратор Высшее образование 1987 Военный инженерный Кра... 07.05.2019 09:50 Не указано
freq 18 4064 3099 1261 30026 22727 3 935 2062 4 25 32268
  1. Размерность таблицы
In [ ]:
hh_data.shape
Out[ ]:
(44744, 12)

Преобразование данных¶

  1. Создадим с помощью функции-преобразования новый признак "Образование", который будет иметь 4 категории: "высшее", "неоконченное высшее", "среднее специальное" и "среднее".
In [ ]:
def education(x):
    x = x.split()
    if x[1] == 'образование':
        return ' '.join(x[:1])
    return ' '.join(x[:2])

hh_data['Образование'] = hh_data['Образование и ВУЗ'].apply(education)
hh_data['Образование'] = hh_data['Образование'].astype('category')

Удалим признак 'Образование и ВУЗ'

In [ ]:
hh_data = hh_data.drop(['Образование и ВУЗ'], axis=1)

Сколько соискателей имеет средний уровень образования (школьное образование)?

In [ ]:
hh_data['Образование'].value_counts()
Out[ ]:
Высшее                 33863
Среднее специальное     5765
Неоконченное высшее     4557
Среднее                  559
Name: Образование, dtype: int64
  1. Создадим два столбца Пол, возраст из столбца "Пол, возраст"
In [ ]:
def gender(x):
    x = x.replace('  ', ' ')
    x = x.split(' , ')
    return str(x[0][0])

def age(x):
    x = x.replace('  ', ' ')
    return int(x.split(' , ')[1].split(' ')[0])

hh_data['Пол'] = hh_data['Пол, возраст'].apply(gender)
hh_data['Возраст'] = hh_data['Пол, возраст'].apply(age)

Удалим признак «Пол, возраст» из таблицы.

In [ ]:
hh_data = hh_data.drop(['Пол, возраст'], axis=1)

Сколько процентов женских резюме представлено в наших данных?

In [ ]:
round(len(hh_data[hh_data['Пол'] == 'Ж']) / len(hh_data) * 100, 2)
Out[ ]:
19.07
In [ ]:
hh_data.describe(include='object')
Out[ ]:
ЗП Ищет работу на должность: Город, переезд, командировки Занятость График Опыт работы Последнее/нынешнее место работы Последняя/нынешняя должность Обновление резюме Авто Пол
count 44744 44744 44744 44744 44744 44576 44743 44742 44744 44744 44744
unique 690 14929 10063 38 47 44413 30214 16927 18838 2 2
top 50000 руб. Системный администратор Москва , не готов к переезду , не готов к кома... полная занятость полный день Опыт работы 10 лет 8 месяцев Апрель 2018 — по... Индивидуальное предпринимательство / частная п... Системный администратор 07.05.2019 09:50 Не указано М
freq 4064 3099 1261 30026 22727 3 935 2062 25 32268 36211

Чему равен средний возраст соискателей?

In [ ]:
round(hh_data['Возраст'].mean(), 1)
Out[ ]:
32.2
  1. Преобразуем признак "Опыт работы". Его текущий формат - это: <Опыт работы: n лет m месяцев, периоды работы в различных компаниях…>.

выделим общий опыт работы соискателя в месяцах, новый признак назовем "Опыт работы (месяц)"

In [ ]:
def get_experience(arg):
    if arg is np.nan or arg == 'Не указано':
        return None
    year_words=['год', 'года', 'лет']
    month_words=['месяц', 'месяца', 'месяцев']
    arg_splitted = arg.split(' ')[:7]
    years = 0
    months = 0
    for index, item in enumerate (arg_splitted):
        if item in year_words:
            years = int(arg_splitted[index-1])
        if item in month_words:
            months = int(arg_splitted[index-1])
    return int(years*12 + months)
hh_data['Опыт работы (месяц)'] = hh_data['Опыт работы'].apply(get_experience)

Медианный опыт работы (в месяцах)?

In [ ]:
hh_data['Опыт работы (месяц)'].median()
Out[ ]:
100.0

Удалим столбец «Опыт работы» из таблицы.

In [ ]:
hh_data = hh_data.drop(['Опыт работы'], axis=1)
  1. Создадим отдельные признаки "Город", "Готовность к переезду", "Готовность к командировкам". При этом важно учесть:
  • Признак "Город" должен содержать только 4 категории: "Москва", "Санкт-Петербург" и "город-миллионник", остальные обозначим как "другие".

  • Признак "Готовность к переезду" должен иметь два возможных варианта: True или False.

  • Признак "Готовность к командировкам" должен иметь два возможных варианта: True или False.

Город:

In [ ]:
def city_f(x):
    million_cities = ['Новосибирск', 'Екатеринбург','Нижний Новгород','Казань', 'Челябинск','Омск', 'Самара', 'Ростов-на-Дону', 'Уфа', 'Красноярск', 'Пермь', 'Воронеж','Волгоград']
    x = x.replace('  ', ' ').split(' , ')[0]
    if x == 'Москва':
        return 'Москва'
    if x == 'Санкт-Петербург':
        return 'Санкт-Петербург'
    if x in million_cities:
        return 'город-миллионник'
    return 'другие'

hh_data['Город'] = hh_data['Город, переезд, командировки'].apply(city_f)
hh_data['Город'] = hh_data['Город'].astype('category')

Готовность к переезду:

In [ ]:
def removal_f(x):
    x = x.replace('  ', ' ').split(' , ')
    if x[1][0] == 'м':
        x = x[2]
    else:
        x = x[1]
    x = x.split(' ')
    for i in x:
        if i == 'не':
            return False
    return True

hh_data['Готовность к переезду'] = hh_data['Город, переезд, командировки'].apply(removal_f)

Готовность к командировкам:

In [ ]:
def business_trips_f(x):
    if x is np.nan:
        return False
    x = x.replace('  ', ' ').split(' , ')
    if len(x) < 3 or x[1][0] == 'м' and len(x) <= 3:
        return False
    if x[1][0] == 'м':
        x = x[3]
    else:
        x = x[2]
    x = x.split(' ')
   
    for i in x:
        if i == 'не':
            return False
    return True

hh_data['Готовность к командировкам'] = hh_data['Город, переезд, командировки'].apply(business_trips_f)

Сколько процентов соискателей живут в Санкт-Петербурге?

In [ ]:
hh_data[hh_data['Город'] == 'Санкт-Петербург'].shape[0] / hh_data.shape[0] * 100
Out[ ]:
11.033881637761487

Сколько процентов соискателей готовы одновременно и к переездам, и к командировкам?

In [ ]:
hh_data[(hh_data['Готовность к переезду'] == True) & (hh_data['Готовность к командировкам'] == True)].shape[0] / hh_data.shape[0] * 100
Out[ ]:
31.88136956910424

Удалим столбец "Город, переезд, командировки"

In [ ]:
hh_data = hh_data.drop(['Город, переезд, командировки'], axis=1)
  1. Рассмотрим поближе признаки "Занятость" и "График". Сейчас признаки представляют собой набор категорий желаемой занятости (полная занятость, частичная занятость, проектная работа, волонтерство, стажировка) и желаемого графика работы (полный день, сменный график, гибкий график, удаленная работа, вахтовый метод).

Такой вариант признаков имеет множество различных комбинаций, а значит множество уникальных значений, что мешает анализу.

Преобразуем категориальные признаки в One Hot Encoding

In [ ]:
employments = ['полная занятость', 'частичная занятость',
              'проектная работа', 'волонтерство', 'стажировка']
charts = ['полный день', 'сменный график', 
         'гибкий график', 'удаленная работа',
         'вахтовый метод']
for employment, chart in zip(employments, charts):
    hh_data[employment] = hh_data['Занятость'].apply(lambda x: employment in x)
    hh_data[chart] = hh_data['График'].apply(lambda x: chart in x)

Сколько людей ищут проектную работу и волонтёрство?

In [ ]:
hh_data[hh_data['проектная работа'] & hh_data['волонтерство']].shape[0]
Out[ ]:
436

Сколько людей хотят работать вахтовым методом и с гибким графиком?

In [ ]:
hh_data[hh_data['вахтовый метод'] & hh_data['гибкий график']].shape[0]
Out[ ]:
2311

Удалим столбцы «Занятость» и «График»

In [ ]:
hh_data = hh_data.drop(['Занятость','График'], axis=1)
  1. Соискатель указывает зарплату в различный валютах, преобразуем заработную плату в единую валюту, например, в рублях. Возникает вопрос, а где взять курс валют по отношению к рублю?

Сделаем выгрузку курсов валют, которые встречаются в наших данных за период с 29.12.2017 по 05.12.2019. И сохраним в csv файл.

Создайте новый DataFrame из полученного файла. В полученной таблице будут столбцы:

  • "currency" - наименование валюты в ISO кодировке,
  • "date" - дата,
  • "proportion" - пропорция,
  • "close" - цена закрытия (последний зафиксированный курс валюты на указанный день).

В признаке "Обновление резюме", содержится дата и время, когда соискатель выложил текущий вариант своего резюме. По дате и будем сопоставлять курсы валют.

Считаем данные курса из файла

In [ ]:
exch_data = pd.read_csv('ExchangeRates.csv', sep=',')

Переведём признак "Обновление резюме" из таблицы с резюме в формат datetime и создадим столбец с датой. В тот же формат приведем признак "date" из таблицы с валютами.

In [ ]:
hh_data['date'] = pd.to_datetime(hh_data['Обновление резюме'], dayfirst=True).dt.date
exch_data['date'] = pd.to_datetime(exch_data['date'], dayfirst=True).dt.date

Выделим из столбца "ЗП" сумму желаемой заработной платы и наименование валюты, в которой она исчисляется. Наименование валюты переведем в стандарт ISO.

In [ ]:
def get_salary_sum(x): # получаем зп
    return float(x.split(' ')[0]) 


def get_salary_currency(arg): # получаем курс
    currency_dict = {
        'USD': 'USD', 'KZT': 'KZT',
        'грн': 'UAH', 'белруб': 'BYN',
        'EUR': 'EUR', 'KGS': 'KGS',
        'сум': 'UZS', 'AZN': 'AZN',
        'руб': 'RUB'
    }
    curr = arg.split(' ')[1].replace('.', '')
    return currency_dict[curr]

hh_data['ЗП (tmp)'] = hh_data['ЗП'].apply(get_salary_sum)
hh_data['Курс (tmp)'] = hh_data['ЗП'].apply(get_salary_currency)

Присоединим к таблице с резюме таблицу с курсами по столбцам с датой и названием валюты. Значение close для рубля заполним единицей (курс рубля самого к себе)

In [ ]:
merged = hh_data.merge(
    exch_data, 
    left_on=['Курс (tmp)', 'Обновление резюме'],
    right_on=['currency', 'date',], 
    how='left'
)
merged['close'] = merged['close'].fillna(1) # заполним пропуски
merged['proportion'] = merged['proportion'].fillna(1)

Умножим сумму желаемой заработной платы на присоединенный курс валюты (close) и разделить на пропорцию.

In [ ]:
hh_data['ЗП (руб)'] = merged['close'] * merged['ЗП (tmp)'] / merged['proportion']

Чему равна желаемая медианная заработная плата соискателей?

In [ ]:
hh_data['ЗП (руб)'].median()/1000
Out[ ]:
60.0

Удалим исходный столбец заработной платы "ЗП" и все промежуточные столбцы.

In [ ]:
hh_data = hh_data.drop(['ЗП', 'ЗП (tmp)', 'Курс (tmp)','date'], axis=1)

Исследование зависимостей в данных¶

In [ ]:
import plotly.express as px
import plotly.graph_objs as go
  1. Построим распределение признака "Возраст".
In [ ]:
fig = px.histogram(
    data_frame=hh_data,
    x='Возраст',
    title='Распределение Возраст соискателей',
    width=500,
    marginal='box',
)
fig.show()

Вывод:

  • среднее значение возраста - 30 лет.
  • предельные значения - от 14 до 100, большинство соискателей от 21 до 41 года
  • имеются аномалии - возраст 100 лет, 77 и 76
  1. Построим распределение признака "Опыт работы (месяц)".
In [ ]:
fig = px.histogram(
    data_frame=hh_data,
    x='Опыт работы (месяц)',
    title='Распределение опыта работы соискателей',
    width=500,
    marginal='box',
)
fig.show()

Вывод:

  • среднее значение опыта - 100 месяцев(8,33 года).
  • предельные значения - от 1 до 1188 месяцев, большинство соискателей имеют опыт от 5 до 160 месяцев
  • имеются аномалии - 1188 месяцев(99 лет)
  1. Построим распределение признака "ЗП (руб)".
In [ ]:
fig = px.histogram(
    data_frame=hh_data,
    x='ЗП (руб)',
    title='Распределение желаемой з/п соискателей',
    width=500,
    marginal='box'
)
fig.show()

Вывод:

  • среднее значение зп - 60т рублей.
  • предельные значения - от 1 до 8.5м рублей, большинство соискателей хотят от 36т до 100т рублей
  • имеются аномалии - зп в размере 1 рубль и также зп от 1 миллиона, но зависит от должности
  1. Построим диаграмму, которая показывает зависимость медианной желаемой заработной платы ("ЗП (руб)") от уровня образования ("Образование").
In [ ]:
temp_data = hh_data[hh_data['ЗП (руб)']<1e6].groupby('Образование', as_index=False).median()
fig = px.bar(
    data_frame=temp_data,
    x='Образование',
    y='ЗП (руб)',
    title='Медианная з/п по уровню образования'
)
fig.show()
C:\Users\rustem\AppData\Local\Temp\ipykernel_13520\2951648698.py:1: FutureWarning:

The default value of numeric_only in DataFrameGroupBy.median is deprecated. In a future version, numeric_only will default to False. Either specify numeric_only or select only columns which should be valid for the function.

Вывод: Можно сделать вывод, что от образования зависит зп, для высшего оно наибольшее, для среднего и среднего специального зп наименьшее

  1. Построим диаграмму, которая показывает распределение желаемой заработной платы ("ЗП (руб)") в зависимости от города ("Город").
In [ ]:
temp_data = hh_data[hh_data['ЗП (руб)']<1e6]
fig = px.box(
    data_frame=temp_data,
    x='Город',
    y='ЗП (руб)',
    title='Распределение з/п по городам'
)
fig.show()

Вывод: Можно заметить, что зп зависит от города, в городах-миллионниках зп меньше, по сравнению с Санкт-Петербургом и Москвой. В "Других" зп наибольшая, так как в этой категории все остальные города России.

  1. Построим график, которая показывает зависимость медианной заработной платы ("ЗП (руб)") от признаков "Готовность к переезду" и "Готовность к командировкам".
In [ ]:
temp_data = hh_data.groupby(
    ['Готовность к командировкам', 'Готовность к переезду'],
    as_index=False
)['ЗП (руб)'].median()
fig = px.bar(
    data_frame=temp_data,
    y='Готовность к переезду',
    x='ЗП (руб)',
    barmode="group",
    color='Готовность к командировкам',
    title='Медианная з/п по готовности к командировкам/переезду'
)
fig.show()

Вывод: Уровень зп зависит от возможности командировок и переездов - чем больше вариаций, тем больше зарплата

  1. Построим карту иллюстрирующую зависимость медианной желаемой заработной платы от возраста ("Возраст") и образования ("Образование").
In [ ]:
temp_data = hh_data.pivot_table(
    index='Образование',
    columns='Возраст',
    values='ЗП (руб)',
    aggfunc='median',
    fill_value=0
)
fig = px.imshow(
    temp_data,
    aspect='auto',
    color_continuous_scale='reds',
    title='Медианная з/п по образованию и возрасту'
)
fig.show()

Вывод: Для высшего образования рост зп происходит быстрее всего и больше, чем в других категориях. Также неоконченное высшее цениться больше, чем среднее образование. Видим аномалию в зп - для высшего образования в возрасте 17 лет средняя зарплата 505т

Для большей наглядности можем убрать все аномалии - выберем зарплату, меньшую, чем 1000000 рублей

In [ ]:
temp_data = hh_data[hh_data['ЗП (руб)'] < 1000000]
temp_data = temp_data.pivot_table(
    index='Образование',
    columns='Возраст',
    values='ЗП (руб)',
    aggfunc='median',
    fill_value=0
)
fig = px.imshow(
    temp_data,
    aspect='auto',
    color_continuous_scale='reds',
    title='Медианная з/п по образованию и возрасту',
)
fig.show()
  1. Построим график, показывающую зависимость опыта работы ("Опыт работы (месяц)") от возраста ("Возраст"). Также построим прямую с координатами (0, 0) и (100, 100).Точки, лежащие на этой прямой и выше неё, — аномалии в наших данных (опыт работы больше либо равен возрасту соискателя).
In [ ]:
x = [0, 100]
import seaborn as sns
temp_data = hh_data.copy()
temp_data['Опыт работы (год)'] = temp_data['Опыт работы (месяц)']/12
fig = px.scatter(
    temp_data, 
    x='Возраст',
    y='Опыт работы (год)',
    title = 'Зависимость опыта работы от возраста')
fig.add_trace(go.Scatter(x=x, y=x))
fig.show();

Вывод: Из нашего графика можно сделать вывод, что в БД присутсвтуют 7 аномалий, связанные с оптытом работы

  1. Построим диаграмму, показывающий зависимость между признаком ("Пол") и зп("ЗП (руб)").
In [ ]:
temp_data = hh_data[hh_data['ЗП (руб)']<1e6]
fig = px.box(
    data_frame=temp_data,
    x='Пол',
    y='ЗП (руб)',
    title='Распределение з/п по полам'
)
fig.show()

Вывод: Можно заметить, что мужчины в среднем зарабатывают больше, чем женщины, также зарплата у мужчин имеют больший размах

  1. Построим диаграмму, которая показывает, какое кол-во людей ищут стажировку, в зависимоти от возраста человека
In [ ]:
temp_data = hh_data[hh_data['стажировка'] == True]
fig = px.histogram(
    data_frame=hh_data,
    x='Возраст',
    title='Распределение стажировок по возрастам',
    width=500,
    marginal='box'
)
fig.show()

Вывод: С повышением возраста, кол-во людей, которые ищут стажировку уменьшаются

Очистка данных¶

  1. Найдием полные дубликаты в таблице с резюме и удалим их.
In [ ]:
duplicates = hh_data[hh_data.duplicated(subset=hh_data.columns)]
hh_data = hh_data.drop_duplicates()
duplicates.shape[0]
Out[ ]:
158
  1. Выведем информацию о числе пропусков в столбцах.
In [ ]:
hh_data.isnull().sum()
Out[ ]:
Ищет работу на должность:            0
Последнее/нынешнее место работы      1
Последняя/нынешняя должность         2
Обновление резюме                    0
Авто                                 0
Образование                          0
Пол                                  0
Возраст                              0
Опыт работы (месяц)                168
Город                                0
Готовность к переезду                0
Готовность к командировкам           0
полная занятость                     0
полный день                          0
частичная занятость                  0
сменный график                       0
проектная работа                     0
гибкий график                        0
волонтерство                         0
удаленная работа                     0
стажировка                           0
вахтовый метод                       0
ЗП (руб)                             0
dtype: int64
  1. Eсть пропуски в 3ех столбцах: "Опыт работы (месяц)", "Последнее/нынешнее место работы", "Последняя/нынешняя должность". Поступим следующим образом: удалиv строки, где есть пропуск в столбцах с местом работы и должностью. Пропуски в столбце с опытом работы заполним медианным значением.
In [ ]:
hh_data = hh_data.dropna(subset=['Последнее/нынешнее место работы', 'Последняя/нынешняя должность'])
hh_data['Опыт работы (месяц)'] = hh_data['Опыт работы (месяц)'].fillna(hh_data['Опыт работы (месяц)'].median())

Теперь удалим выбросы и аномалии¶

  1. Удалим резюме, в которых указана заработная плата либо выше 1 млн. рублей, либо ниже 1 тыс. рублей.
In [ ]:
delete_data = hh_data[(hh_data['ЗП (руб)'] > 1e6) | (hh_data['ЗП (руб)'] < 1e3)]
hh_data = hh_data.drop(delete_data.index)
print(f'Удалено {delete_data.shape[0]} записей')
Удалено 435 записей
  1. Удалим резюме, в которых опыт работы в годах превышает возраст соискателя.
In [ ]:
delete_data = hh_data[hh_data['Опыт работы (месяц)']/12 >= hh_data['Возраст']]
hh_data = hh_data.drop(delete_data.index)
print(f'Удалено {delete_data.shape[0]} записей')
Удалено 7 записей
  1. В результате анализа мы обнаружили потенциальные выбросы в признаке "Возраст". Это оказались резюме людей чересчур преклонного возраста для поиска работы. Попробуем построить распределение признака в логарифмическом масштабе. Добавим к графику линии, отображающие среднее и границы интервала метода трех сигм.

Найдём выбросы с помощью метода z-отклонения и удалим их из данных, используя логарифмический масштаб.

Выведим таблицу с полученными выбросами и оценим их.

In [ ]:
fig, ax = plt.subplots(1, 1, figsize=(8, 4))
log_age = np.log(hh_data['Возраст'] + 1)
histplot = sns.histplot(log_age, bins=30, ax=ax)
histplot.axvline(log_age.mean(), color='k', lw=2)
histplot.axvline(log_age.mean()+ 4 *log_age.std(), color='k', ls='--', lw=2)
histplot.axvline(log_age.mean()- 3 *log_age.std(), color='k', ls='--', lw=2)
histplot.set_title('Log Age Distribution');

def outliers_z_score_mod(data, feature, left=3, right=4, log_scale=False):
    if log_scale:
        x = np.log(data[feature]+1)
    else:
        x = data[feature]
    mu = x.mean()
    sigma = x.std()
    lower_bound = mu - left * sigma
    upper_bound = mu + right * sigma
    delete_data = data[(x < lower_bound) | (x > upper_bound)]
    cleaned = data[(x >= lower_bound) & (x <= upper_bound)]
    return delete_data, cleaned
delete_data, hh_data = outliers_z_score_mod(hh_data, 'Возраст', left=3,  right=4, log_scale=True)
print(f'Удалено {delete_data.shape[0]} записей')
delete_data['Возраст']
Удалено 3 записей
Out[ ]:
31137     15
32950     15
33654    100
Name: Возраст, dtype: int64

Вывод: График асимметричен в левую сторону, под категорию выбросов попадают люди с возрастом 15, 15 и 100 лет